Skip to content

feat: migrate experiments config from array to bools#398

Open
mwbrooks wants to merge 5 commits intomainfrom
mwbrooks-config-experiments-type
Open

feat: migrate experiments config from array to bools#398
mwbrooks wants to merge 5 commits intomainfrom
mwbrooks-config-experiments-type

Conversation

@mwbrooks
Copy link
Member

@mwbrooks mwbrooks commented Mar 14, 2026

Changelog

  • N/A

Summary

This pull request migrates the experiments config format from an array to a map of boolean values. It includes backwards-compatible JSON unmarshaling that will auto-migrate config.json files.

Example

Before:

{
  "experiments": ["sandboxes"],
  "manifest": {
    "source": "local"
  },
  "project_id": "..."
}

After:

{
  "experiments": {
    "sandboxes": true,
    "charm" :false
  },
  "manifest": {
    "source": "local"
  },
  "project_id": "..."
}

Motivation

Why on earth are you doing this? 👨🏻‍🎨 🎨 🚲 🏠

The motivation is to unblock adding a slack config command to get and set system-level and project-level config files.

The config command will use a dot-notation to get and set key values:

$ slack config get manifest.source       # Getter
$ slack config set manifest.source local # Setter

When the <value> is a simple type, it works well, such as string, number, or boolean

$ slack config set manifest.source "local"  # String
$ slack config set some.timeout 10000       # Number
$ slack config set some.toggle false        # Boolean

When the <value> is a complex type, it's not intuitive or simple, such as arrays

# Which format do we support? It will require regex parsing in most cases
$ slack config set experiments experiment1, experiment2, experiment3
$ slack config set experiments ["experiment1", "experiment2", "experiment3"]
$ slack config set experiments ['experiment1', 'experiment2', 'experiment3']

# Does this append experiment1 to the array or replace the array with experiment1?
$ slack config set experiments experiment1

In order for setting [value] to be simple and intuitive, we should restrict the config.json files to use simple value types that can be expressed on the command-line (e.g. string, number, and boolean). Arrays are complex and error-prone to express (["value1", "value2"] or value1, value2) and all elements must be included each time.

This is why this pull request migrates the experiments to an object of key: boolean pairs.

An added bonus is that the new format naturally supports disabling experiments from the config file. 🧠

Requirements

Change the experiments field in SystemConfig, ProjectConfig, and Config
from []experiment.Experiment to map[string]bool. This enables dot-notation
access for the upcoming config command and allows explicit enable/disable
of experiments.

Includes backwards-compatible JSON unmarshaling that auto-converts the old
array format (["charm"]) to the new map format ({"charm": true}).

Updates help output to show all available experiments with ENABLED/DISABLED
status for better discoverability.
Reverts the EXPERIMENTS section changes from the experiments
migration commit to keep PR #1 focused on the array-to-map
refactor. Help menu improvements will be done in a separate PR.
@mwbrooks mwbrooks added this to the Next Release milestone Mar 14, 2026
@mwbrooks mwbrooks self-assigned this Mar 14, 2026
@mwbrooks mwbrooks added enhancement M-T: A feature request for new functionality experiment Experimental feature accessed behind the --experiment flag or toggle semver:patch Use on pull requests to describe the release version increment labels Mar 14, 2026
@mwbrooks mwbrooks changed the title feat: migrate experiments config from array to map feat: migrate experiments config from array to bools Mar 14, 2026
@codecov
Copy link

codecov bot commented Mar 14, 2026

Codecov Report

❌ Patch coverage is 85.29412% with 10 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@e47334c). Learn more about missing BASE report.
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/config/experiments.go 90.32% 2 Missing and 1 partial ⚠️
internal/config/experiments_unmarshal.go 80.00% 2 Missing and 1 partial ⚠️
internal/config/project.go 81.81% 1 Missing and 1 partial ⚠️
internal/config/system.go 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #398   +/-   ##
=======================================
  Coverage        ?   67.91%           
=======================================
  Files           ?      219           
  Lines           ?    18103           
  Branches        ?        0           
=======================================
  Hits            ?    12294           
  Misses          ?     4649           
  Partials        ?     1160           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mwbrooks mwbrooks marked this pull request as ready for review March 16, 2026 18:43
@mwbrooks mwbrooks requested a review from a team as a code owner March 16, 2026 18:43
Copy link
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks LGTM! This is an amazing change for improved configurations overall - too often I changed "bolt" to "bolts" for confident testing ⚡ 🏁

I'm leaving a few questions related to the order of detecting experiments as well as question about typings. I also don't think we should preserve backward compatibilities for experiments since that adds meaningful logic otherwise but no blockers 🎆


// Feature experiments
ExperimentsFlag []string
experiments []experiment.Experiment
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 question: Can we continue to use strict types of experiment or is this not allowed as a key?

	experiments     map[experiment.Experiment]bool

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zimeg Great suggestion and I should have caught that myself. I think we can use map[experiment.Experiment]bool, so let me look into refactoring it 💻

Comment on lines 35 to 38
// Load from flags
for _, flagValue := range c.ExperimentsFlag {
experiments = append(experiments, experiment.Experiment(flagValue))
experiments[flagValue] = true
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️‍🗨️ thought: The order of experiment parsing might need to be reversed so that project and user configurations can be overridden with a flag if the lower settings use a "false" value?

Comment on lines -43 to +42
jsonContents := []byte(fmt.Sprintf("{\"last_update_checked_at\":\"2023-05-11T15:41:07.799619-07:00\",\"experiments\":[\"%s\"]}", validExperiment))
jsonContents := []byte(fmt.Sprintf(`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":{"%s":true}}`, validExperiment))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ praise: Thanks for improving both the string formatting and experiments formatting here!

Comment on lines +52 to +71
t.Run("Correctly finds experiments from old array format in config.json", func(t *testing.T) {
// Setup
ctx, fs, _, config, pathToConfigJSON, teardown := setup(t)
defer teardown(t)
_, mockPrintDebug := setupMockPrintDebug()

// Write old array format
jsonContents := []byte(
fmt.Sprintf(
`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":["%s"]}`,
validExperiment,
),
)
_ = afero.WriteFile(fs, pathToConfigJSON, jsonContents, 0600)

config.LoadExperiments(ctx, mockPrintDebug)
experimentOn := config.WithExperimentOn(validExperiment)
assert.Equal(t, true, experimentOn)
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t.Run("Correctly finds experiments from old array format in config.json", func(t *testing.T) {
// Setup
ctx, fs, _, config, pathToConfigJSON, teardown := setup(t)
defer teardown(t)
_, mockPrintDebug := setupMockPrintDebug()
// Write old array format
jsonContents := []byte(
fmt.Sprintf(
`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":["%s"]}`,
validExperiment,
),
)
_ = afero.WriteFile(fs, pathToConfigJSON, jsonContents, 0600)
config.LoadExperiments(ctx, mockPrintDebug)
experimentOn := config.WithExperimentOn(validExperiment)
assert.Equal(t, true, experimentOn)
})

🪓 quibble(non-blocking): We might not fear breaking experimental configuration setups since this can become burdensome to keep in mind ongoing?

Copy link
Member Author

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, like a dummy I forgot to publish my comments for the reviewers 🤦🏻


// Write our test script to the memory file system used by the mock
jsonContents := []byte(fmt.Sprintf("{\"last_update_checked_at\":\"2023-05-11T15:41:07.799619-07:00\",\"experiments\":[\"%s\"]}", validExperiment))
jsonContents := []byte(fmt.Sprintf(`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":{"%s":true}}`, validExperiment))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Just updating the formatting to make the tests more readable with literals and no escapes.

assert.Contains(t, mockOutput.String(), "active project experiments: []")
assert.Contains(t, mockOutput.String(), fmt.Sprintf("active permanently enabled experiments: [%s]", experiment.Placeholder))
assert.Equal(t, []experiment.Experiment{experiment.Placeholder}, config.experiments)
assert.ElementsMatch(t, []experiment.Experiment{experiment.Placeholder}, config.GetExperiments())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Ordering now varies because it's a map instead of slice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality experiment Experimental feature accessed behind the --experiment flag or toggle semver:patch Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants